package cx3k.dataaccess.search;

import java.lang.reflect.Field;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import cx3k.dataaccess.CX3KDatabaseManager;
import cx3k.dataaccess.StandardDataAccessService;
import cx3k.dataaccess.search.Search.HQLQuery;
import cx3k.dataaccess.search.SearchValue.EqualityType;
import cx3k.scheduler.objects.Course;
import cx3k.scheduler.objects.GroupedCourse;
import cx3k.scheduler.objects.Major;
import cx3k.scheduler.objects.MeetingTime;
import cx3k.scheduler.objects.RequirementGrouping;
import cx3k.scheduler.objects.Semester;

/**
 * The Searcher provides the ability to search persistent storage based 
 * on field values passed into the program.  The Searcher will build a query,
 * and attempt to execute the query, returning the results to the user.
 * 
 * This class depends on the HibernateUtil accessible from CX3KDatabaseManager.
 * 
 * @author Chris Thierer
 * @see cx3k.dataaccess.CX3KDatabaseManager 
 */
public class Searcher {
	
	/**
	 * Constant representing the day Monday, as stored in persistent storage.
	 */
	public static final String MONDAY = "M";
	
	/**
	 * Constant representing the day Tuesday, as stored in persistent storage.
	 */
	public static final String TUESDAY = "Tu";
	
	/**
	 * Constant representing the day Wednesday, as stored in persistent storage.
	 */
	public static final String WEDNESDAY = "W";
	
	/**
	 * Constant representing the day Thursday, as stored in persistent storage.
	 */
	public static final String THURSDAY = "Th";
	
	/**
	 * Constant representing the day Friday, as stored in persistent storage. 
	 */
	public static final String FRIDAY = "F";
	
	/**
	 * Constant representing the day Saturday, as stored in persistent storage.
	 */
	public static final String SATURDAY = "Sa";
	
	/**
	 * Constant representing the day Sunday, as stored in persistent storage.
	 */
	public static final String SUNDAY = "Su";
	
	/**
	 * Represents a field that can be searched for using this Searcher. 
	 * 
	 * @author Chris Thierer
	 *
	 */
	public enum SearchField {
		/** Search for a Course at the given level */
		LEVEL		("level", Course.class, "courseNum", "and"),
		/** Search for a Meeting Time based on the day */
		DAY			("day", MeetingTime.class, "day", "and"),
		/** Search for a Course based on the discipline specified */
		DISCIPLINE	("discipline", Course.class, "degreeProgram", "and"),
		/** Search for a Course based on if a course is required or not */
		REQUIRED	("required", RequirementGrouping.class, "requirementGroupings", "and"),
		/** Search for a Course based on course ID */
		COURSE_ID	("course ID", Course.class, "courseId", "and"),
		/** Search for a Semester based on semester ID */
		SEMESTER_ID	("semester ID", Semester.class, "semesterId", "and"),
		/** Search for a Semester based on semester session type */
		SESSION_TYPE("semester type", Semester.class, "sessionType", "and"),
		/** Search for a Semester based on semester year */
		SEMESTER_YR	("semester year", Semester.class, "year", "and"),
		/** Search for a Grouped Course based on course name */
		GROUPED_COURSE_DEGREE_PROGRAM	("grouped course degree program", GroupedCourse.class, "degreeProgram", "and"),
		/** Search for a Grouped Course based on course number */
		GROUPED_COURSE_COURSE_NUM		("grouped course number", GroupedCourse.class, "courseNum", "and"),
		/** Search for a Major based on major ID */
		MAJOR_ID	("major id", Major.class, "majorId", "and")
		;
		
		/**
		 * The English name of this field.
		 */
		private String fieldName;
		
		/**
		 * The class to which the field belongs.
		 */
		private Class<?> clazz;
		
		/**
		 * The field to search for a specified value.
		 */
		private Field field;
		
		/**
		 * The boolean operator to use to associate this field with another
		 * field ("and", "or")
		 */
		private String operator;

		/**
		 * Construct the Search Field enumerated object with the required 
		 * class parameters.
		 * @param fieldName The English name of this field.
		 * @param clazz The class to which this field belongs.
		 * @param field The field to search for a specified value.
		 * @param operator The boolean operator to use to associated this field
		 * 			with another field ("and", "or).
		 */
		private SearchField(String fieldName, Class<?> clazz, String field, String operator){
						
			try {
				this.fieldName = fieldName;
				this.clazz = clazz;
				this.field = getFieldClass().getDeclaredField(field);
				this.operator = operator;
			} catch (SecurityException e) {
				// TODO log this
				
				this.field = null;
			} catch (NoSuchFieldException e) {
				// TODO log this
				
				this.field = null;
			}
		}
		
		/**
		 * @return The field to search for a specified value.
		 */
		private Field getField(){
			return field;
		}
		
		/**
		 * @return The class to which the searched field belongs.
		 */
		private Class<?> getFieldClass(){
			return clazz;
		}
		
		/**
		 * @return The English name of this Search Field.
		 */
		public String getFieldName(){
			return fieldName;
		}
		
		/**
		 * @return The Boolean operator used to associated this search 
		 * 			field with subsequent ones ("and", "or").
		 */
		private String getOperator(){
			return operator;
		}
	}
	
	/**
	 * A value being searched for, associated with a Search Field.  Defines the 
	 * value being searched for (as a String), as well as the equality operator
	 * to use.
	 * 
	 * @author Chris Thierer
	 * @see cx3k.dataacess.search.Searcher.SearchField
	 */
	public class SearchValue {
		
		/**
		 * The value being searched for.
		 */
		private String value;
		
		/**
		 * The equality operator.
		 */
		private EqualityType operator;
		
		/**
		 * Construct a search value with the given value, and the default
		 * equality operator of EqualityType.LIKE.
		 * @param value The value to be searched for.
		 * @see cx3k.dataaccess.search.SearchValue.EqualityType.LIKE 
		 */
		public SearchValue(String value){
			this(value, EqualityType.LIKE);
		}
		
		/**
		 * Construct a search value with the given value, and the specified
		 * equality operator.
		 * @param value The value to be searched for.
		 * @param operator The operator to use when searching storage.
		 * @see cx3k.dataacess.search.SearchValue.EqualityType
		 */
		public SearchValue(String value, EqualityType operator){
			this.value = value;
			this.operator = operator;
		}
		
		/**
		 * @return The equality operator to use when searching storage.
		 */
		private EqualityType getOperator(){
			return operator;
		}
		
		/**
		 * @return The value to be searched for.
		 */
		public String getValue(){
			return value;
		}
		
	}
	
	/**
	 * Helper method to create a mapping of Class objects to their corresponding
	 * fields, i.e., group fields that belong to the same Class together.
	 * @param fields The array of fields to search through.
	 * @return The mapping of Class objects to Search Fields.
	 */
	private static Map<Class<?>, Collection<SearchField>> initClassMappings(SearchField[] fields){
		// create the mapping
		Map<Class<?>, Collection<SearchField>> mapping = 
			new HashMap<Class<?>, Collection<SearchField>>();
		
		// go through each field
		for(SearchField field : fields){
			
			// check if the field is already in the mapping or not
			if(mapping.containsKey(field.getFieldClass())){
				// already in mapping: add the field to the existing list
				mapping.get(field.getFieldClass()).add(field);
			} else {
				// not in mapping: create a new list, and then add the field to it
				Collection<SearchField> newCollection = new LinkedList<SearchField>();
				newCollection.add(field);
				
				mapping.put(field.getFieldClass(), newCollection);
			}
		}
		
		// return the generated mapping
		return mapping;
	}
	
	/**
	 * @return An array of degree programs, matching the majors in the database.
	 */
	public static String[] getDegreeProgramNames(){
		
//		Searcher searcher = new Searcher(Course.class);
		String[] degreePrograms = {"AFST", "AGNG", "AMST", "ANCS", "ANTH", 
				"APMB", "ARBC", "ARCH", "ART", "ATPH", "BIOL", "BTEC", "CHEM", 
				"CHIN", "CMPE", "CMSC", "DANC", "ECAC", "ECAD", "ECON", "ECPA", 
				"EDUC", "EENG", "EHS", "ELC", "ENCE", "ENCH", "ENEE", "ENES", 
				"ENGL", "ENME", "ENMG", "FREN", "GERM", "GERO", "GES", "GREK", 
				"GWST", "HAPP", "HCC", "HCST", "HEBR", "HIST", "HONR", "HUM", 
				"IS", "JDST", "JPNS", "KORE", "LAS", "LATN", "LING", "LLC", 
				"MAED", "MATH", "MCS", "MEES", "MUSC", "PHED", "PHIL", 
				"PHYS", "POLI", "PSYC", "PUB", "PUBL", "RLST", "RUSS", 
				"SCI", "SOCY", "SPAN", "STAT"};
		
		return degreePrograms;
		/*Collection<Major> majors = StandardDataAccessService.getStandardDataAccessService().getAllMajors().values();
		String[] degreePrograms = new String[majors.size()];
		int pos = 0;
		
		for(Major major : majors){
			degreePrograms[pos++] = major.getProgram();
		}
		
		return degreePrograms;*/
	}
	
	public static void main(String[] args){
		Searcher searcher = new Searcher(Course.class);
		
		searcher.searchForValueLike(SearchField.DISCIPLINE, "CMSC");
		searcher.searchForValueNotLike(SearchField.DAY, "Tu");
		searcher.searchForValueNotLike(SearchField.DAY, "Mo");
		searcher.searchForValueNotLike(SearchField.DAY, "We");
		searcher.searchForValueBetween(SearchField.LEVEL, "99", "200");
//		searcher.searchForValueBetween(SearchField.LEVEL, "300", "400");
		
		searcher.execute();
	}
	
	/**
	 * Mapping of values to fields, used to generate the query.  
	 */
	private Map<SearchField, Collection<SearchValue>> valueMapping = 
		new HashMap<SearchField, Collection<SearchValue>>();
	
	/**
	 * Type of Object to be searched for in the query.
	 */
	private Class<?> searchFor;
	
	/**
	 * Static table mapping Classes to Fields, grouping Fields that belong 
	 * to the same class together.  
	 */
	private static Map<Class<?>, Collection<SearchField>> classMapping = 
		initClassMappings(SearchField.values());
	
	/**
	 * Construct this Searcher object to return type of the Class specified.
	 * @param searchFor The type of Class to search for.
	 */
	public Searcher(Class<?> searchFor){
		this.searchFor = searchFor;
	}
	
	/**
	 * Construct this Semester based on the type of the initialized Object
	 * passed in.
	 * @param instanceOf This instance is used to generate the type of Class
	 * 			to be used in this Searcher.
	 */
	public Searcher(Object instanceOf){
		this.searchFor = instanceOf.getClass();
	}
	
	/**
	 * Build an HQL Query based on the class passed in.
	 * @param ofType Type to be returned by the Query.
	 * @return The HQL Query object representing the generated HQL query.
	 */
	private HQLQuery buildQuery(Class<?> ofType){
		
		// start the query based on the specified type
		// FROM <ofType> as <oftype>
		HQLQuery query = getSearch(ofType).getHQLQuery();
		
		// temporary variable used to hold the Search objects built
		Search result = null;
		
		// get all classes from the static mapping
		Set<Class<?>> classes = classMapping.keySet();
		
		// remove the class that was already added to the query
		classes.remove(ofType);
		
		// for each class in the mapping
		for(Class<?> clazz : classes){
			
			// create the Search for the class specified
			result = createSearch(clazz);
			
			// if the query is null, and the result is not
			if(query == null && result != null){
				// this is the first result, use it to build the query
				query = result.getHQLQuery();
			} else if(query!= null && result != null) {
				// the result is not null, append this to the query
				query.join(result.getHQLQuery());
			}
		}
		
		// the generated HQL query
		return query;
	}
	
	/**
	 * Clear the generated query (remove all values).
	 */
	public void clear(){
		valueMapping.clear();
	}
	
	/**
	 * Clear only one field (remove values from one field).
	 * @param field The field to be cleared.
	 */
	public void clearField(SearchField field){
		valueMapping.remove(field);
	}
	
	/**
	 * Create a Search object based on the provided class, and the fields
	 * stored for that class in the query table.
	 * @param clazz The class to build the query around.
	 * @return The generated Search object encapsulating the query, or null
	 * 			if no fields were specified for the Class specified.
	 */
	private Search createSearch(Class<?> clazz){
		
		// build the Search object for the class
		// FROM <Clazz> as <clazz>
		Search searcher = new Search(clazz);
		
		// flag variable to store if something has been added yet
		boolean add = false;
		
		// protect from a null value being encountered 
		try{
			// get the fields associated with this class, and go through each one
			for(SearchField field : classMapping.get(clazz)){
				
				// check if the query table contains this field
				if(valueMapping.containsKey(field)){
					
					// go through each value associated this field
					for(SearchValue value : valueMapping.get(field)){
						
						// check if this is the first value added
						if(!add){
							// yes, so just add the value to query
							searcher.addToQuery(field.getField(), 
									value.getValue(), value.getOperator());
							
							// set the flag
							add = true;
						} else {
							// no, so determine which boolean variable to use to join them
							if(field.getOperator().equals("and")){
								// join fields together with "and"
								searcher.addAndToQuery(field.getField(), 
										value.getValue(), value.getOperator());
							} else {
								// join fields together with "or"
								searcher.addOrToQuery(field.getField(), 
										value.getValue(), value.getOperator());
							}
						}
					}
				}
			}
			
		} catch(NullPointerException e){
			// TODO log this error
		}
		
		// if something as been added to the query
		if(add){
			// return the Search object
			return searcher;
		} else {
			// no fields were added, no point in querying, return null
			return null;
		}
	}
	
	/**
	 * Execute the query generated.  The List returned is of a generic type,
	 * because although the HQL generated will try to only return one 
	 * Class type, it is possible that by querying for n unrelated tables
	 * within the database, n types of values will be returned (i.e., if 
	 * searching for Profile and Meeting Times, which are unrelated, the 
	 * query will return an array in each list location with a Profile
	 * Object and a Meeting Time Object).   
	 * @return The List of results from the persistent storage.
	 */
	public List<?> execute(){
		String HQL = buildQuery(getSearchFor()).getHQL();
		System.out.println("executing " + HQL);
		classMapping = initClassMappings(SearchField.values());
		return CX3KDatabaseManager.executeHQL(HQL);
	}
	
	/**
	 * Build a Search object for a given class.
	 * @param forClass The class to use to build the Search Object.
	 * @return The resulting Search object.
	 */
	private Search getSearch(Class<?> forClass){
		
		// the search object to be created
		Search search;
		
		// the fields associated with this class
		Collection<SearchField> fields = classMapping.get(forClass);
		
		// if there are no fields
		if(fields == null || fields.isEmpty()){
			// build the search based on just this class
			search = new Search(forClass);
		} else {
			// need to build the search including the fields
			search = createSearch(forClass);
			
			// no fields were added for some reason
			if(search == null){
				// build the search based on just this class
				search = new Search(forClass);
			}
		}
		
		// return the resulting Search object
		return search;
	}
	
	/**
	 * @return The type being searched for in the persistent storage.
	 */
	private Class<?> getSearchFor(){
		return searchFor;
	}
	
	/**
	 * Search for the given field and value using the specified operator.  It 
	 * will not execute the search, it will only add the information to the 
	 * query table, which will be used to build the HQL query.  
	 * @param field The field being searched.
	 * @param value The value being searched for.
	 * @param operator The operator used to compare the value to information
	 * 			in persistent storage.
	 */
	private void searchFor(SearchField field, String value, EqualityType operator){

		// check if the query table already contains the field 
		if(valueMapping.containsKey(field)){
			// it does, just add this value to field list
			valueMapping.get(field).add(new SearchValue(value, operator));
		} else {
			// it does not, create a new collection of fields, add this to it
			Collection<SearchValue> newCollection = new LinkedList<SearchValue>();
			newCollection.add(new SearchValue(value, operator));
			
			valueMapping.put(field, newCollection);
		}
	}
	
	/**
	 * Search for a value that is greater than the value specified 
	 * (WHERE field > value).
	 * @param field The field to search.
	 * @param value The value to form the lower bounds.
	 */
	public void searchForBiggerValue(SearchField field, String value){
		searchFor(field, value, EqualityType.GREATER_THAN);
	}
	
	/**
	 * Search for a value that is exactly the value specified
	 * (WHERE field = value).
	 * @param field The field to search.
	 * @param value The value that must be matched.
	 */
	public void searchForExactValue(SearchField field, String value){
		searchFor(field, value, EqualityType.EQUALS);
	}
	
	/**
	 * Search for a value that is smaller than the value specified 
	 * (WHERE field < value).
	 * @param field The field to search.
	 * @param value The value to form the upper bounds.
	 */
	public void searchForSmallerValue(SearchField field,String value){
		searchFor(field, value, EqualityType.LESS_THAN);
	}
	
	/**
	 * Search for a value that is between the two values specified
	 * (WHERE field > value1 and field < value2).  
	 * This assumes that value 1 is less than value 2, otherwise the 
	 * query will be generated, but the results will not be as expected.
	 * @param field The field to search.
	 * @param value1 The value to form the lower bounds.
	 * @param value2 The value to form the upper bounds.
	 */
	public void searchForValueBetween(SearchField field, String value1, String value2){
		searchForBiggerValue(field, value1);
		searchForSmallerValue(field, value2);
	}
	
	/**
	 * Search for a value that is like the value specified 
	 * (WHERE field like '%value%'), which means that the field need only 
	 * contain the sequence of characters specified by the value, surrounded
	 * by any other information.  
	 * @param field The field to search.
	 * @param value The value to look for.  
	 */
	public void searchForValueLike(SearchField field, String value){
		searchFor(field, value, EqualityType.LIKE);
	}
	
	/**
	 * Search for a value that is not like the value specified 
	 * (WHERE not field like '%value%'), which means that the field need
	 * only contain the sequence of characters specified by the value,
	 * surrounded by any other information.
	 * @param field The field to search.
	 * @param value The value to search for.
	 */
	public void searchForValueNotLike(SearchField field, String value){
		searchFor(field, value, EqualityType.NOT_LIKE);
	}
	
	/**
	 * Remove the specified value from the query field.  If the field is empty,
	 * then nothing is removed.  This will only remove the first value
	 * the matches the value parameter.  If the field is empty after the value
	 * is removed, then the field is removed from the query completely. 
	 * @param field The field to remove the value from.
	 * @param value The value to remove.
	 */
	public void remove(SearchField field, String value){
		
		try{
			// get iterator of values
			Iterator<SearchValue> values = valueMapping.get(field).iterator();
			
			// one value from the iterator 
			SearchValue aValue;
			
			// flag to control loop
			boolean notFound = true;
			
			// while there are values, and the value has not been found yet
			while(values.hasNext() && notFound){
				// get the next value
				aValue = values.next();
				
				// if the value equals the value being searched for
				if(aValue.getValue().equals(value)){
					// remove it
					values.remove();
					
					// set the flag to stop looping
					notFound = false;
				}
			}
			
			// check if the field now has no values 
			if(valueMapping.get(field).isEmpty()){
				// remove the field from the query (no values)
				valueMapping.remove(field);
			}
		} catch(NullPointerException e){
			//TODO log this
			// result if the field has not been added to the query
		}
	}
}
